package cbbx_sm.simulation;

import java.awt.Dimension;
import java.awt.Toolkit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import java.util.Map.Entry;

import cbbx_sm.decision_maker.CameraConfiguration;
import cbbx_sm.decision_maker.Decision;
import cbbx_sm.decision_maker.IDecisionMaker;
import cbbx_sm.endtoend.EndToEndTestUtils;
import cbbx_sm.parser.CameraData;
import cbbx_sm.parser.Entity;
import cbbx_sm.parser.Frame;
import cbbx_sm.probabilistic_model.Cluster;
import cbbx_sm.probabilistic_model.ClusterAttributes;
import cbbx_sm.probabilistic_model.Clustering;
import cbbx_sm.probabilistic_model.IPredictor;
import cbbx_sm.probabilistic_model.Prediction;
import cbbx_sm.probabilistic_model.ProbabilisticModel;
import cbbx_sm.probabilistic_model.SystemShortTermMemory;
import cbbx_sm.probabilistic_model.SystemState;
import cbbx_sm.utils.ExperimentManager;
import cbbx_sm.utils.Timestamp;
import cbbx_sm.utils.view.FramePanel;
import cbbx_sm.utils.view.Viewer;

/**
 * 
 * @author Alessio Della Motta - University of California, Irvine
 * 
 */
public class Scheduler {
	/* ***********************************************************
	 * System State, Probabilistic & Decision Model.
	 * ***********************************************************/
	// Noisy-or predictor window, so it is the same of the past state
	// list size.
	private final static int VIEW_UPDATE_RATE = 500;
	private String OUTPUT_FILENAME;
	private static boolean firstRun = true;
	
	// List of all clusters and data arre useful for viewer
	private List<CameraData> cameraDatas;
	
	// This variable takes into account the time of simulation, in order
	// to coordinate frame reproduction and extraction from camera.
	private Timestamp currentTime;
	private Timestamp initialTime;
	private Timestamp finalTime;
	
	// Structure for loading events and system states 
	private HashMap<Timestamp, ArrayList<ClusterAttributes>> timeEvents = new HashMap<Timestamp, ArrayList<ClusterAttributes>>();
	
	// Structure for holding the frames by timestamp for the demo visualization.
	private List<HashMap<Timestamp, Frame>> timeFrames = new ArrayList<HashMap<Timestamp, Frame>>();
	
	// Probability model
	private ProbabilisticModel probModel;
	private SystemShortTermMemory memory;
	private IPredictor predictor;
	private IDecisionMaker decisionMaker;

	// This variable represents the last prediction done.
	private Prediction curPrediction;
	
	// this variable represent, for the current prediction prediction, 
	// the instant of time this prediction is supposed to be valid.
	//private Timestamp timeOfPrediction;
	
	// The decision or plan of the next step of data collection
	private Decision curDecision;
	/* ***********************************************************/
	
	/* ***********************************************************
	 * Statistics, Benefit & Cost
	 * ***********************************************************/
	// This variable indicate if at this time of reproduction, our
	// old prediction has an hit (that is the predicted cluster contains
	// an entity). It is useful to update hits and misses value at the
	// end of reproduction of each time instant from all cameras.
	private int highReshits = 0;
	private int lowReshits = 0;
	private int misses = 0;
	/* ***********************************************************/
	
	/* ***********************************************************
	 * View
	 * ***********************************************************/
	private List<Viewer> viewers = new Vector<Viewer>();
	/* ***********************************************************/
	
	
	public Scheduler(List<CameraData> cameraDatas, ProbabilisticModel probModel,
			String filename, String imageFolder){
		this.OUTPUT_FILENAME = filename;
		this.probModel = probModel;
	
		this.curPrediction = null;
		this.curDecision = new Decision(new Vector<CameraConfiguration>());
		this.cameraDatas = prepareCameraData(cameraDatas);
		
		// Prepare the data structures for the scheduler simulator.
		Map<String, List<Cluster>> clusters = new HashMap<String, List<Cluster>>();
		for (CameraData cameraData :  cameraDatas) {
			clusters.put(cameraData.getCameraId(), new ArrayList<Cluster>());
		}
		for (Cluster c : probModel.getFusedClusters()) {
			clusters.get(c.getCameraId()).add(c);
		}


		for (int i = 0; i < cameraDatas.size(); i++){
			
			CameraData data = cameraDatas.get(i);
			
			if (ExperimentManager.isViewEnabled){
				// Setting the current time as the smallest timestamp of all camera data, so
				// it will be the timestamp of the oldest frame above all cameras.
				// Yes, you can move it out of the loop for optimization but it 
				// won't run on the linux cluster as there is no screen attached.
				Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
				int numInCol = 2;
				int numInRow = 2;
				assert(numInCol == numInRow);
				int height = dim.height / numInCol;
				int width = dim.width / numInRow;
				
				Viewer viewer = new Viewer(data, clusters.get(data.getCameraId()), data.getImageFile(), imageFolder, width, height);
				viewer.setLocation(10 + width * (i/numInRow) , 10 + height*(i%numInCol));
				viewers.add(viewer);
			}
		}
		initialTime = ProbabilisticModel.getGlobalInitialTimeStamp(cameraDatas);
		finalTime = ProbabilisticModel.getGlobalFinalTimeStamp(cameraDatas);

		loadOffLineStreamEvents(cameraDatas, clusters);
	}
	
	/**
	 * Removes the events from before noon for the illustrative example generation.
	 * @param cameraDatas
	 * @return
	 */
	private List<CameraData> prepareCameraData(List<CameraData> cameraDatas) {		
		for (CameraData camData : cameraDatas) {
			if (ExperimentManager.isViewEnabled) {
				List<Frame> newFrames = new ArrayList<Frame>();
				for (Frame f : camData.getFrames()) {
					// 20090528155300L
					// 20090528002600L
					// 20090528000000L
					// 20090528150000L
					if (Timestamp.subtractTimestamps(f.getTimestamp(), new Timestamp(20090528164550L)) > 0) {
						newFrames.add(f);
					}
				}
				camData.setFrames(newFrames);
			}	
		}
		return cameraDatas;
	}

	private void loadOffLineStreamEvents(List<CameraData> cameraDatas, Map<String, List<Cluster>> clusters){
		System.out.println("Starting loading off-line streams: " + System.currentTimeMillis());
		
		for (int cameraIndex = 0; cameraIndex < cameraDatas.size(); cameraIndex++){
			CameraData data = cameraDatas.get(cameraIndex);
			timeFrames.add(new HashMap<Timestamp, Frame>());
			List<Frame> frames = data.getFrames();
			List<Cluster> cameraClusters = clusters.get(data.getCameraId());
			int currentFrame = 0;
			for (Timestamp time = initialTime; time.compareTo(finalTime) < 0; time = Timestamp.getNextTimeStamp(time)){
				while ((currentFrame < frames.size())
						&& frames.get(currentFrame).getTimestamp().compareTo(time) < 0) {
					currentFrame ++;
				}
				if (currentFrame >= frames.size()) {
					break;
				}
				if (frames.get(currentFrame).getTimestamp().compareTo(time) > 0) {
					continue;
				}
				Frame frame = frames.get(currentFrame);
				
				 if (ExperimentManager.isViewEnabled) {
					 timeFrames.get(cameraIndex).put(time, frame);
				 }
				
				// If the current frame contains some entities...
				for (Entity e : frame.getEntities()){
					// Getting the cluster containing the current entity
					Cluster owner = 
						Clustering.getFutureOwningCluster(
								cameraClusters, e.getBoxes()[ExperimentManager.boxLocationId]);
					
					// Setting the attributes for this cluster in order to update the
					// system state (the only attribute is that if the cluster contains
					// an entity or not).
					ClusterAttributes curAttributes = new ClusterAttributes();
					curAttributes.setContainsEntity(true);
					curAttributes.setCluster(owner);
					
					if (!timeEvents.containsKey(time)){
						timeEvents.put(time, new ArrayList<ClusterAttributes>());
					}
					timeEvents.get(time).add(curAttributes);
				}
			}
		}
		System.out.println("Loading off-line streams ended: " + System.currentTimeMillis());
	}
	
	public void sequentialRun(){
		this.curPrediction = null;
		this.curDecision = new Decision(new Vector<CameraConfiguration>());
		currentTime = initialTime;
		highReshits = 0;
		lowReshits = 0;
		misses = 0;
		long decisionMakingTime = 0;
		int numberOfDecisions = 0;
		int prevHighResHits = 0;
		
		
		// While there is a camera with frames to play, we
		// let to play that camera...
		while(!isFinished()){
			// Just for one timestamp
			SystemState currentSystemState = createCurrentSystemState(currentTime);
			memory.updateMemory(currentSystemState);
			
			// Updating statistics for current time instant
			updateStatisticsForCurrentTime(currentTime);
			
			//if (prevHighResHits < highReshits) {
			if (highReshits > 0)
				if (ExperimentManager.debug) {
					System.out.printf("%s\t%s\t%s\t%s%n", currentTime, curPrediction.toString(), decisionMaker.getClass().getName(), curDecision.toString());
				}
			//}
			
			// Updating Views
			if (ExperimentManager.isViewEnabled){
				updateView();
			}
			
			long timeBeforeDecision = System.currentTimeMillis();
			
			// Making prediction complete for the current time
			makeNextPrediction(currentTime);
			
			// Make a decision for the next step of data collection
			curDecision = decisionMaker.makeDecision(curPrediction);
			
			decisionMakingTime += System.currentTimeMillis() - timeBeforeDecision;
			numberOfDecisions ++;
			
			// Updating the timestamp for next time instant
			currentTime = Timestamp.getNextTimeStamp(currentTime);
			
			prevHighResHits = highReshits;
		}
		
		EndToEndTestUtils.printResults(OUTPUT_FILENAME, firstRun, probModel.getThreshold(), probModel.getFusedClusters().size(),
				predictor.toString(), decisionMaker.toString(), highReshits, lowReshits, misses, ((double) decisionMakingTime / numberOfDecisions));
		if (firstRun){
			firstRun = false;
		}
	}
	
	private void makeNextPrediction(Timestamp currentTime){
		// Making the new prediction for the current time instant.
		curPrediction = predictor.makePrediction(currentTime);
		
		// If we have been up, use what seen not prediction when predicting current state.
		if (ExperimentManager.timeInFuture == 0 && ExperimentManager.useSeenAsPrediction) {
			HashMap<Cluster, Double> clusterProbabilities = 
				curPrediction.getClusterProbabilities();
			SystemState pastState = memory.getSystemState(currentTime);
			for (Entry<Cluster, Double> entry : clusterProbabilities.entrySet()) {
				if (curDecision.cameraZoomedIn(entry.getKey().getCameraId()) == false) {
					if (pastState.getAttributes(entry.getKey()) == null) {
						entry.setValue(0d);	
					}
				}
			}
			for (CameraConfiguration cc : curDecision.getDecision()) {
				Cluster c = cc.getCluster();
				if (pastState.getAttributes(c) == null) {
					clusterProbabilities.put(c, 0d);
				}
			}	
		}
			
		curPrediction.setTimestamp(Timestamp.getNextTimeStamp(currentTime));
	}
	
	private SystemState createCurrentSystemState(Timestamp currentTime){
		HashMap<Cluster, ClusterAttributes> tempMap = new HashMap<Cluster, ClusterAttributes>();
		
		List<ClusterAttributes> attributes = timeEvents.get(currentTime);
		SystemState currentSystemState;
		
		if (attributes == null) {
			currentSystemState = new SystemState(currentTime, tempMap);
			return currentSystemState;
		}
		
		for (ClusterAttributes attr : attributes){
			Cluster curCluster = attr.getCluster();
			String cameraId = curCluster.getCameraId();
			
			// If we zoom out we see the whole picture.
			if (curDecision.cameraZoomedIn(cameraId) == false){
				tempMap.put(curCluster, attr);
			}
			// If we zoom in we see only that region.
			else if (curDecision.clusterChosenInCamera(cameraId) == curCluster){
				tempMap.put(curCluster, attr);
			}
		}
		
		currentSystemState = new SystemState(currentTime, tempMap);
		return currentSystemState;
	}
	
	/**
	 * If all camera data don't contain any frame, it means we have
	 * finished to play camera streams.
	 * 
	 * @return if playing is finished or not.
	 */
	private boolean isFinished(){
		if (currentTime.compareTo(finalTime) > 0) return true;
		return false;
	}
	
	/**
	 * Given current system state and last prediction, we verify our
	 * prediction and update hits and misses.
	 * 
	 * @param timestamp
	 */
	private void updateStatisticsForCurrentTime(Timestamp timestamp){
		SystemState currentState = memory.getSystemState(timestamp);
		
		// Compute the number of entities collected at high resolution.
		for (CameraConfiguration conf : curDecision.getDecision()){
			Cluster cluster = conf.getCluster();
			if (cluster != null){
				ClusterAttributes attr = currentState.getAttributes(cluster);
				
				if (attr == null){
					misses++;
					continue;
				}
				else{
					if (attr.containsEntity()) highReshits++;
					else misses++;
				}
			} 
		}
		
		// Compute the number of entities collected at low resolution.
		
		HashMap<Cluster, ClusterAttributes> seen = currentState.getClusterStates();
		for (Cluster cluster: seen.keySet()){
			ClusterAttributes attr = seen.get(cluster);
			String cameraId = cluster.getCameraId();
			if (curDecision.cameraZoomedIn(cameraId)==false && attr.containsEntity()){
				lowReshits++;
			}
		}
		
	}
		
	//--------------------
	// View functions
	//--------------------
	private void updateView() {
		HashMap<String, Frame> currentFrames = new HashMap<String, Frame>();
		
		for (int cameraIndex = 0; cameraIndex < cameraDatas.size(); cameraIndex++){
			Frame currentFrame = timeFrames.get(cameraIndex).get(currentTime);
			if (currentFrame != null) {
				currentFrames.put(currentFrame.getCameraId(), currentFrame);	
			}
		}
		
		for (Viewer viewer : viewers){
			String cameraId = viewer.getCameraId();
			CameraConfiguration curConf = null;
			Cluster predictedCluster = null;
						
			for (CameraConfiguration conf : curDecision.getDecision())
				if (conf.getCameraId().equals(cameraId)){
					curConf = conf;
					break;
				}
			
			if (curConf != null) predictedCluster = curConf.getCluster();
			
			Frame curFrame = currentFrames.get(cameraId);
			updateViewer(viewer.getPanel(), curFrame, predictedCluster);
		}
		
		// Waiting for some time if there are some
		// frames at currentTime...
		if (currentFrames.size() > 0){
			try{ Thread.sleep(VIEW_UPDATE_RATE); }
			catch(InterruptedException e){
				e.printStackTrace();
			}
		}
	}
	
	private void updateViewer(FramePanel panel, Frame curFrame, 
							  Cluster predictedCluster){
		
		panel.setTimestamp(currentTime);
		panel.setPrediction(predictedCluster);
		panel.setHits(highReshits);
		panel.setPrediction(curPrediction);
		panel.setMisses(misses);
		
		if (curFrame != null){
			panel.setCurFrame(curFrame);
			
			if (probModel.isClusterContainingABBox(probModel.getFusedClusters(), predictedCluster, curFrame)){
				panel.setActualCluster(predictedCluster);
			}
			else {
				Cluster actualCluster;
				if (curFrame.getEntities().size() > 0){
					actualCluster = ProbabilisticModel.getOwningCluster(
							probModel.getFusedClusters(), 
							curFrame.getEntities().get(0).getBoxes()[ExperimentManager.boxLocationId], curFrame.getCameraId());
					panel.setActualCluster(actualCluster);
				}
				else panel.setActualCluster(null);
			}

			// It's an asynchronous call, so it is not correct because
			// next I set the new prediction. I want a synchronous painting...
			if (panel.getActualCluster() != null) {
				panel.paintImmediately(0, 0, panel.getWidth(), panel.getHeight());				
			}
		} else {
			panel.setCurFrame(null);
			panel.setActualCluster(null);
			// It's an asynchronous call, so it is not correct because
			// next I set the new prediction. I want a synchronous painting...
			panel.paintImmediately(0, 0, panel.getWidth(), panel.getHeight());
		}
	}
	

	
	// Getters & Setters
	
	public SystemShortTermMemory getMemory() {
		return memory;
	}

	public void setMemory(SystemShortTermMemory memory) {
		this.memory = memory;
	}
	
	public IPredictor getPredictor() {
		return predictor;
	}

	public void setPredictor(IPredictor predictor) {
		this.predictor = predictor;
	}

	public IDecisionMaker getDecisionMaker() {
		return decisionMaker;
	}

	public void setDecisionMaker(IDecisionMaker decisionMaker) {
		this.decisionMaker = decisionMaker;
	}
}
